package MusicLandscape.container;

import MusicLandscape.entities.Track;
import MusicLandscape.util.MyMatcher;

import java.util.*;

/**
 * Represents a set of tracks and a (possibly empty) subset of those tracks which are selected.
 * <p>
 * This class is a container for unique tracks. It does not accept null tracks, nor can a track which is already
 * contained be added again. Additionally, it supports the notion of selection, meaning that some tracks can be
 * selected. The selection is a subset of all tracks currently held by the container. The selection may at times be
 * empty, and it may at other times contain all tracks.
 * <p>
 * The container provides methods to filter, sort, and retrieve the selection.
 * <p>
 * Tracks can only be removed from this container by removing all currently selected tracks. Removing tracks therefore
 * requires the creation of a proper selection first. The usual process of creating a selection is:
 *
 * <ol>
 *   <li>select ALL tracks (by resetting the selection)</li>
 *   <li>(possibly repeatedly) filter the selection with a matcher. The filter is applied to the current selection!</li>
 *   <li>if desired, sort the selection.</li>
 *   <li>remove selected tracks from container OR retrieve the selection as an array of tracks.</li>
 * </ol>
 *
 * @author Jonas Altrock (ew20b126@technikum-wien.at)
 * @version 1
 * @since ExerciseSheet05
 */
public class MyTrackContainer {

    /**
     * The selected tracks in this container.
     * Initially empty.
     */
    private final List<Track> selection = new ArrayList<>();

    /**
     * The tracks in this container.
     * Initially empty.
     */
    private final Set<Track> tracks = new HashSet<>();

    //
//

    /**
     * Creates a default MyTrackContainer.<br>
     * A default container has no tracks and an empty selection.
     */
    public MyTrackContainer() {
    }

    /**
     * Creates a container from an iterable object of tracks.<br>
     * All tracks of the argument are added to this container.
     * <p>
     * Initially, all tracks are selected.
     *
     * @param t the iterable object of tracks to be added to this container.
     */
    public MyTrackContainer(Iterable<Track> t) {
        for (Track track : t) {
            tracks.add(track);
            selection.add(track);
        }
    }

    /**
     * Creates a container from an array of tracks.<br>
     * All tracks of the argument are added to this container.
     * <p>
     * Initially, all tracks are selected.
     *
     * @param t the array of tracks to be added to this container.
     */
    public MyTrackContainer(Track[] t) {
        this(List.of(t));
    }

    /**
     * Add a single track.<br>
     * The argument is attempted to be added to this container.
     * <p>
     * If successfully added, it is NOT added to the selection. Tracks already added cannot be added again.
     * Null tracks cannot be added either.
     *
     * @param t the track to add
     * @return whether the argument could be added
     */
    public boolean add(Track t) {
        if (t == null) {
            return false;
        }
        if (tracks.contains(t)) {
            return false;
        }
        tracks.add(t);
        return true;
    }


    /**
     * Bulk operation to add tracks.<br>
     * All tracks of the argument are added to this container.
     *
     * @param t the tracks to add
     * @return the number of tracks added
     */
    public int addAll(Track[] t) {
        int added = 0;

        for (Track track : t) {
            if (add(track)) {
                added++;
            }
        }

        return added;
    }


    /**
     * Filters the selection.<br>
     * Applies the filter defined by the argument to the selection, keeping only those elements that match.
     * <p>
     * The filter is applied to the selection and the selection only, i.e. the selection cannot grow in size during
     * this operation. If all elements of a selection match the specified filter, the selection remains unchanged.
     *
     * @param matcher the filter defining which of the tracks of the selection to keep.
     * @return the number of elements removed from the selection during this operation.
     */
    public int filter(MyMatcher<Track> matcher) {
        int removed = 0;

        for (Iterator<Track> it = selection.iterator(); it.hasNext(); ) {
            Track t = it.next();

            if (!matcher.matches(t)) {
                it.remove();
                removed++;
            }
        }

        return removed;
    }

    /**
     * Removes the selected tracks from this container.<br>
     * All currently selected tracks are removed from this container.
     * <p>
     * After this operation all remaining tracks are selected (the selection is reset).
     *
     * @return the number of removed tracks
     */
    public int remove() {
        int removed = selection.size();

        for (Track track : selection) {
            tracks.remove(track);
        }

        reset();
        return removed;
    }

    /**
     * Resets the selection, thereby selecting ALL tracks in this container.
     */
    public void reset() {
        selection.retainAll(List.<Track>of());
        selection.addAll(tracks);
    }

    /**
     * Gets the selected tacks.<br>
     * The currently selected tracks of this container are returned as an array of tracks.
     * <p>
     * The tracks are returned in their current order.
     * If the selection is empty an array of size 0 is returned.
     *
     * @return the selected tracks.
     */
    public Track[] selection() {
        return selection.toArray(new Track[0]);
    }


    /**
     * The number of tracks currently held by this container.<br>
     * Note: this is not the size of the selection.
     *
     * @return the number of tracks
     */
    public int size() {
        return tracks.size();
    }


    /**
     * Sorts the selection of tracks of this container.<br>
     * The currently selected tracks are sorted in the sense defined by the first argument.
     * <p>
     * The second argument controls the scheme (ascending/descending order).
     *
     * @param theComp the comparator defining the sorting order
     * @param asc     the sorting scheme. true stands for ascending (from smallest to highest element) false for descending.
     */
    public void sort(Comparator<Track> theComp, boolean asc) {
        selection.sort(asc ? theComp : theComp.reversed());
    }
}
